using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Text;
using System.Windows;
using System.Windows.Media.Media3D;
using System.Windows.Media;
using System.Media;
using NewtonDynamics;
using System.Diagnostics;
using IrrKlang;

namespace WPFPhysics1
{
	public enum BodyType { Rectangle, Ellipse, Polygon, Car, end };
    public enum BodyCountSliderType { current, max, min, end };


	public class Bodies : ObservableCollection<CBody>
	{
		private const double depth = 5f;

		private CWorld world = new CWorld();

        private DateTime? lastUpdate;
		private Collection<CBody> removed = new Collection<CBody>();

		protected double worldWidth;
		protected double worldHeight;

        //array to hold the body count data. this is used when bodies are dropped into the world
        public int [,] BodyCountSliderData;

        //variables to hold the mouse action in the world. these are use to act on bodies in the world
        protected String mousedownbodyid, mouseupbodyid,mousewheelbodyid;
        protected Point mouselocationatclick, mousecurrentlocation, mouselocationatclickrelativetoobject;
        int mousewheeldelta;

        protected BodyActionType bodyaction;

        // create material, physis and special effects for the collision callback
        private CMaterial bodymaterial;
        private CMaterialPhysics bodymaterialphysics;

        //HACK Need to understand what this really does. For now, create an empty one.
        private CMaterialPhysicsSpecialEffect bodyspecialeffects = new CMaterialPhysicsSpecialEffect();
        
        System.Windows.Media.Media3D.Vector3D v1, v2;

        bool newcontact = false;
        String contactb1,contactb2;

        MediaPlayer theplayer = new MediaPlayer();
        ISoundEngine soundengine = new ISoundEngine();

        int soundalias = 0;

        Random alpharand = new Random();

        public Bodies(double worldWidth, double worldHeight)
		{
            ISoundSource soundsource;
            
            this.worldWidth = worldWidth;
			this.worldHeight = worldHeight;

			world.SetSize( new System.Windows.Media.Media3D.Vector3D( -worldWidth / 2, -worldHeight / 2, -2 ), new System.Windows.Media.Media3D.Vector3D( worldWidth / 2, worldHeight / 2, 2 ) );
/*			world.SetFrictionModel( EFrictionModel.ExactModel );
			world.SetSolverModel( ESolverModel.ExactMode );*/

			world.BodyLeaveWorld += new EventHandler<CBodyLeaveWorldEventArgs>( world_BodyLeaveWorld );

            // initialize the slider counts based on body type

            BodyCountSliderData = new int[(int)BodyType.end,(int)BodyCountSliderType.end];

            BodyCountSliderData[(int)BodyType.Car,(int)BodyCountSliderType.max]=4;
            BodyCountSliderData[(int)BodyType.Car,(int)BodyCountSliderType.min]=1;
            BodyCountSliderData[(int)BodyType.Car,(int)BodyCountSliderType.current]=1;

            BodyCountSliderData[(int)BodyType.Rectangle, (int)BodyCountSliderType.max] = 10;
            BodyCountSliderData[(int)BodyType.Rectangle, (int)BodyCountSliderType.min] = 1;
            BodyCountSliderData[(int)BodyType.Rectangle, (int)BodyCountSliderType.current] = 5;

            BodyCountSliderData[(int)BodyType.Ellipse, (int)BodyCountSliderType.max] = 10;
            BodyCountSliderData[(int)BodyType.Ellipse, (int)BodyCountSliderType.min] = 1;
            BodyCountSliderData[(int)BodyType.Ellipse, (int)BodyCountSliderType.current] = 5;

            BodyCountSliderData[(int)BodyType.Polygon, (int)BodyCountSliderType.max] = 10;
            BodyCountSliderData[(int)BodyType.Polygon, (int)BodyCountSliderType.min] = 1;
            BodyCountSliderData[(int)BodyType.Polygon, (int)BodyCountSliderType.current] = 5;

            mousedownbodyid = "";
            mouseupbodyid = "";
            mousewheelbodyid = "";

            // use the default material that is created automatically
            bodymaterial = new CMaterial(world, true);

            // create the object for managing the physics in the world
            bodymaterialphysics = new CMaterialPhysics(world);

            // there is only one material, set the callbacks for this combination of materials
            // if more materials are added, then additional callbacks will be needed
            bodymaterialphysics.SetCollisionCallback(bodymaterial, bodymaterial, bodyspecialeffects, body_contactBegin, body_contactProcess, body_contactEnd);

            theplayer.Open(new Uri("click.wav", UriKind.Relative));
            
            soundengine.AddSoundSourceFromFile("explosion.wav", StreamMode.NoStreaming, true);
            soundsource = soundengine.AddSoundSourceFromFile("click.wav", StreamMode.NoStreaming, true);

            for (int i = 0; i < 100; i++)
            {
                soundengine.AddSoundSourceAlias(soundsource, i.ToString());
            }

        }

		/// <summary>
		/// Body left world.  Remove from ObservableCollection and leave for GC.
		/// </summary>
		/// <param name="sender">World</param>
		/// <param name="e">Event arguments</param>
		private void world_BodyLeaveWorld( object sender, CBodyLeaveWorldEventArgs e )
		{
			if ( e.Body != null )
			{
				Remove( e.Body );
			}
		}

		public void SetPlatforms()
		{
			Collection<Body> platforms = new Collection<Body>();

            foreach (Body body in this)
            {
                if (body is StaticRectangleBody)
                {
                    platforms.Add(body);
                }
            }
			foreach ( StaticRectangleBody platform in platforms ) Remove( platform );

			Random rand = new Random();

			AddRectangle( -2, -1, 7, 0.15f, 15, true );

			for ( int ledge = 0; ledge < 10; ++ledge )
			{
				AddRectangle(
					( -4.5 + ( rand.NextDouble() * 9 ) ),
					( -2 + ( rand.NextDouble() * 6 ) ),
					( 1 + ( rand.NextDouble() * 3 ) ),
					0.15f,
					( rand.NextDouble() * 60 ) - 30,
					true );
			}
		}

		/// <summary>
		/// Drop objects
		/// </summary>
		public void DropObjects( BodyType type )
		{
			Random rand = new Random();
			int maxItems = BodyCountSliderData[(int)type,(int)BodyCountSliderType.current];

			for ( int index = 0; index < maxItems; ++index )
			{
				double x = -4.5 + ( 9 * ( (double)index / maxItems ) );
				double y = -4;

				switch ( type )
				{
					case BodyType.Rectangle:
						AddRectangle( x, y, 0.40, 0.40, rand.NextDouble() * 90f, false );
						break;

					case BodyType.Ellipse:
						AddEllipse( x, y, 0.40, 0.35, rand.NextDouble() * 90f );
						break;

					case BodyType.Polygon:
						AddPolygon( x, y, new Vector[] { new Vector( 0, 0.20 ), new Vector( -0.20, -0.20 ), new Vector( 0.20, -0.20 ) } );
						break;
                    
                    case BodyType.Car:
                        AddCar(x,y, 2, 0.75);
                        break;
                }
			}

		}

		/// <summary>
		/// Add rectangle
		/// </summary>
		/// <param name="x">X offset</param>
		/// <param name="y">Y offset</param>
		/// <param name="width">Width</param>
		/// <param name="height">Height</param>
		/// <param name="angle">Angle (degrees)</param>
		/// <param name="isStatic">Static?</param>
		public void AddRectangle( double x, double y, double width, double height, double angle, bool isStatic )
		{
			RectangleBody body = RectangleBody.Create(
				world,
				isStatic ? double.PositiveInfinity : 1,
				width,
				height,
				isStatic ? depth * 2 : depth );

			Matrix3D matrix = Matrix3D.Identity;

			// Rotate around the Z axis
			matrix.Rotate( new Quaternion( new System.Windows.Media.Media3D.Vector3D( 0, 0, 1 ), angle ) );

			// Translate to x, y
			matrix.Translate( new System.Windows.Media.Media3D.Vector3D( x, y, 0 ) );

			// Apply matrix
			body.Matrix = matrix;

			if ( !isStatic )
			{
                //randomly select an alphabet for this rectangle
                int letter = alpharand.Next(0,25);
                body.BrushImageSource = "Letter" + Convert.ToChar(Convert.ToByte('A') + letter).ToString() + ".png";
				body.ApplyForceAndTorque += new EventHandler<CApplyForceAndTorqueEventArgs>( body_ApplyForceAndTorque );
			}

			Add( body );
		}

		/// <summary>
		/// Add ellipse
		/// </summary>
		/// <param name="x">X offset</param>
		/// <param name="y">Y offset</param>
		/// <param name="width">Width</param>
		/// <param name="height">Height</param>
		/// <param name="angle">Angle (radians)</param>
		public void AddEllipse( double x, double y, double width, double height, double angle )
		{
			EllipseBody body = EllipseBody.Create(
				world,
				1f,
				width,
				height,
				depth );

			Matrix3D matrix = Matrix3D.Identity;

			// Rotate around the Z axis
			matrix.Rotate( new Quaternion( new System.Windows.Media.Media3D.Vector3D( 0, 0, 1 ), angle ) );

			// Translate to x, y
			matrix.Translate( new System.Windows.Media.Media3D.Vector3D( x, y, 0 ) );

			// Apply matrix
			body.Matrix = matrix;

			body.ApplyForceAndTorque += new EventHandler<CApplyForceAndTorqueEventArgs>( body_ApplyForceAndTorque );

			Add( body );
		}

		/// <summary>
		/// Add polygon
		/// </summary>
		/// <param name="x">X offset</param>
		/// <param name="y">Y offset</param>
		/// <param name="width">Width</param>
		/// <param name="height">Height</param>
		/// <param name="vertices">Array of vertices</param>
		public void AddPolygon( double x, double y, Vector[] vertices )
		{
			PolygonBody body = PolygonBody.Create(
				world,
				1f,
				vertices,
				depth );

			Matrix3D matrix = Matrix3D.Identity;

			// Translate to x, y
			matrix.Translate( new System.Windows.Media.Media3D.Vector3D( x, y, 0 ) );

			// Apply matrix
			body.Matrix = matrix;

			body.ApplyForceAndTorque += new EventHandler<CApplyForceAndTorqueEventArgs>( body_ApplyForceAndTorque );

			Add( body );
		}

		/// <summary>
		/// Add car
		/// </summary>
		/// <param name="x">X offset</param>
		/// <param name="y">Y offset</param>
		/// <param name="width">Width</param>
		/// <param name="height">Height</param>
		/// <returns>Car body</returns>
		public void AddCar( double x, double y, double width, double height )
		{
			// Work in progress - Newton has some basic vehicle modeling built in (suspension etc).  Would be nice to use this (at least to play with).
			// In practice will probably just figure how to attach static axles to a body, a ball joint on the end of each (constrained to the z-axis)
			// and circles or polygons as tires (polygons would be nice 'hand drawn' so the car can roll on lumpy tires like the YouTube vid).

			VehicleBody body = VehicleBody.Create(
				world,
				1500f,
				width,
				height,
				depth );

			Matrix3D matrix = Matrix3D.Identity;

			// Translate to x, y
			matrix.Translate( new System.Windows.Media.Media3D.Vector3D( x, y, 0f ) );

			// Apply transform
			body.Matrix = matrix;

			body.ApplyForceAndTorque += new EventHandler<CApplyForceAndTorqueEventArgs>( body_ApplyForceAndTorque );

			Add( body );

		}

		/// <summary>
		/// Apply force and torque callback
		/// </summary>
		/// <param name="sender">Body</param>
		/// <param name="e">Event arguments</param>
		void body_ApplyForceAndTorque( object sender, CApplyForceAndTorqueEventArgs e )
		{
			CBody body = (CBody)sender;
            float gravity = 9.8f;

            if ((String)(body.UserData) == "noforce")
            {
                gravity = 0;
                body.LinearDamping = 0;
                body.Torque = new System.Windows.Media.Media3D.Vector3D(0, 0, 0);
                body.Omega = new System.Windows.Media.Media3D.Vector3D(0, 0, 0);
                body.AngularDamping = new System.Windows.Media.Media3D.Vector3D(0, 0, 0);
                body.Velocity = new System.Windows.Media.Media3D.Vector3D(0, 0, 0);
                body.LinearDamping = 0;

            }

            body.AddForce( new System.Windows.Media.Media3D.Vector3D( 0, gravity * body.MassMatrix.m_Mass, 0 ) );

		}

        void body_contactBegin(object sender, CContactBeginEventArgs e)
        {

            Body b1 = e.Body0 as Body;
            Body b2 = e.Body1 as Body;


            if ((b1.bodyid != contactb1) || (b2.bodyid != contactb2))
            {
                newcontact=true;
                contactb1=b1.bodyid;
                contactb2=b2.bodyid;
            }

        }

        void body_contactProcess(object sender, CContactProcessEventArgs e)
        {
           
            // Set the material (2 point contact handle)
            bodymaterial.Handle = e.Material;

            System.Windows.Media.Media3D.Vector3D force = bodymaterial.GetContactForce();

            if (force.Length != 0 && newcontact)
            {
                newcontact = false;
                //HACK: Play a sound when the contact is made. Change volume based on force

                if (force.LengthSquared/10000 > .5)
                {

                    String soundname = soundalias.ToString();
                    //soundengine.Play2D(soundname);
                    soundalias = (soundalias == 99) ? 0 : soundalias + 1;
                    
                }
            }


        }

        void body_contactEnd(object sender, CContactEndEventArgs e)
        {
        }
        /// <summary>
		/// Remove item
		/// </summary>
		/// <param name="body">Body to remove</param>
		private new void Remove( CBody body )
		{
			if ( !removed.Contains( body ) )
			{
				base.Remove( body );
				removed.Add( body );
			}
		}

		/// <summary>
		/// Update world
		/// </summary>
		public void Update()
		{
			DateTime time = DateTime.Now;

			world.Update( lastUpdate.HasValue ? (float)( time - lastUpdate.Value ).TotalSeconds : 0 );

			if ( removed.Count > 0 )
			{
				foreach ( CBody body in removed ) body.Dispose();
				removed.Clear();
			}

			lastUpdate = time;
		}

		/// <summary>
		/// Get world width
		/// </summary>
		public double WorldWidth { get { return worldWidth; } }

		/// <summary>
		/// Get world height
		/// </summary>
		public double WorldHeight { get { return worldHeight; } }

        public void AddImpulseToBodyId(String BodyIdToFind,System.Windows.Media.Media3D.Vector3D ForceVector)
        {
            foreach (Body body in this)
            {
                if (body.bodyid == BodyIdToFind)
                {
                    body.AddImpulse(ForceVector, new System.Windows.Media.Media3D.Vector3D(body.Matrix.OffsetX,body.Matrix.OffsetY,body.Matrix.OffsetZ));
                    return;
                }
            }

            return;
        }

        public void moveToBodyId(String BodyIdToFind, System.Windows.Media.Media3D.Vector3D moveDeltaMatrix)
        {
            foreach (Body body in this)
            {
                if (body.bodyid == BodyIdToFind)
                {
                    Application myapp = Application.Current as App;
                    Window mymainwindow = myapp.MainWindow;

                    Matrix3D matrix = body.Matrix;

                    double height = mymainwindow.ActualHeight;
                    double width = height*this.WorldWidth/this.WorldHeight;

                    moveDeltaMatrix.X *= this.worldWidth / width;
                    moveDeltaMatrix.Y *= this.worldHeight / height;

                    //matrix.Translate(moveDeltaMatrix);
                    matrix.OffsetX += moveDeltaMatrix.X;
                    matrix.OffsetY += moveDeltaMatrix.Y;

                    body.Matrix = matrix;

                    body.UserData = "noforce";

                    body.AddImpulse(new System.Windows.Media.Media3D.Vector3D(0,0,0),new System.Windows.Media.Media3D.Vector3D(0,0,0));
                    Update();

                    return;
                }
            }
        }

        public void StopDraggingBody()
        {
            foreach (Body body in this)
            {
                if (body.bodyid == mousedownbodyid)
                {
                    body.UserData="";

                    body.AddImpulse(new System.Windows.Media.Media3D.Vector3D(0, 0, 0), new System.Windows.Media.Media3D.Vector3D(0, 0, 0));

                    return;
                }
            }
        }

       public void DisposeBodyId(String BodyIdToFind)
        {   
            foreach (Body body in this)
            {
                if (body.bodyid==BodyIdToFind)
                {
                    this.Remove(body);
                    return;
                }
            }

            return;
        }

       public void RotateBodyId(String BodyIdToRotate, int AngleDelta)
       {
           foreach (Body body in this)
           {
               if (body.bodyid == BodyIdToRotate)
               {
                   Matrix3D matrix = body.Matrix;

                   body.UserData = "noforce";

                   if (AngleDelta != 0)
                   {
                       matrix.RotateAt(new Quaternion(new System.Windows.Media.Media3D.Vector3D(0, 0, 1), AngleDelta),new Point3D(matrix.OffsetX, matrix.OffsetY, 1));
                       body.Matrix = matrix;
                       body.AddImpulse(new System.Windows.Media.Media3D.Vector3D(0, 0, 0), new System.Windows.Media.Media3D.Vector3D(0, 0, 0));
                       Update();
                   }
               }
           }
       }

       public void RotateStopBodyId(String BodyIdToRotate)
       {
           foreach (Body body in this)
           {
               if (body.bodyid == BodyIdToRotate)
               {
                   body.UserData="";
                   BodyAction = BodyActionType.none;
                   body.AddImpulse(new System.Windows.Media.Media3D.Vector3D(0, 0, 0), new System.Windows.Media.Media3D.Vector3D(0, 0, 0));
               }
           }
       }

        public String mouseDownBodyId
       {
           get
           {
               return mousedownbodyid;
           }
           set
           {
               mousedownbodyid = value;
           }
       }

       public String mouseWheelBodyId
       {
           get
           {
               return mousewheelbodyid;
           }
           set
           {
               mousewheelbodyid = value;
           }
       }

       public Point mouseLocationAtClick
       {
           get
           {
               return mouselocationatclick;
           }
           set
           {
               mouselocationatclick = value;
           }
       }
       public Point mouseLocationAtClickRelativeToObject
       {
           get
           {
               return mouselocationatclickrelativetoobject;
           }
           set
           {
               mouselocationatclickrelativetoobject = value;
           }
       }
       public Point mouseCurrentLocation
       {
           get {return mousecurrentlocation;}
           set {mousecurrentlocation=value; }
       }

       public int mouseWheelDelta
       {
           get { return mousewheeldelta; }
           set { mousewheeldelta = value; }
       }

       public BodyActionType BodyAction
       {
           get
           {
               return bodyaction;
           }
           set
           {
               bodyaction = value;
           }
       }
       public void UIBodyAction()
       {

           double xdelta, ydelta;

           switch (BodyAction)
           {

               case BodyActionType.addimpulse:

                   double radians, angle = 0;
                   double myxforce = 8;
                   double myyforce = 8;
                   double xdirection = 1, ydirection = 1;

                   xdelta = mouseCurrentLocation.X - mouseLocationAtClick.X;
                   ydelta = mouseCurrentLocation.Y - mouseLocationAtClick.Y;

                   if (xdelta != 0)
                   {
                       xdirection = xdelta / Math.Abs(xdelta);
                   }
                   if (ydelta != 0)
                   {
                       ydirection = ydelta / Math.Abs(ydelta);
                   }

                   radians = Math.Atan(ydelta / xdelta);
                   angle = Math.Abs(radians * (180 / Math.PI));

                   System.Windows.Media.Media3D.Vector3D Force = new System.Windows.Media.Media3D.Vector3D(myxforce * xdirection * (90 - angle) / 90, myyforce * ydirection * angle / 90, 0);

                    /* Removing the line below. All impulse will be applied to the centre of the body.
                     * System.Windows.Media.Media3D.Vector3D Point = new System.Windows.Media.Media3D.Vector3D(mouseLocationAtClickRelativeToObject.X, mouseLocationAtClickRelativeToObject.Y, 0);
                     * 
                     */
                   AddImpulseToBodyId(mouseDownBodyId, Force);
                   break;

               case BodyActionType.remove:
                   DisposeBodyId(mouseDownBodyId);
                   soundengine.Play2D("explosion.wav");
                   break;

               case BodyActionType.dragstart:

                   xdelta = (mouseCurrentLocation.X - mouseLocationAtClick.X);
                   ydelta = (mouseCurrentLocation.Y - mouseLocationAtClick.Y);

                   moveToBodyId(mouseDownBodyId, new System.Windows.Media.Media3D.Vector3D(xdelta, ydelta, 0));

                   mouseLocationAtClick = mouseCurrentLocation;

                   break;

               case BodyActionType.dragstop:

                   StopDraggingBody();

                   break;
               case BodyActionType.rotate:

                   RotateBodyId(mouseWheelBodyId, mouseWheelDelta);
                   
                   break;
               case BodyActionType.rotatestop:

                   RotateStopBodyId(mouseWheelBodyId);
                   break;
           }
       }

    }
}